/**
 * Development repository: https://github.com/kaisalmen/WWOBJLoader
 */

// Imports only related to wrapper
import {
  Object3D
} from "../../build/three.module.js";
import {
  CodeBuilderInstructions,
  WorkerExecutionSupport
} from "./obj2/worker/main/WorkerExecutionSupport.js";
import {CodeSerializer} from "./obj2/utils/CodeSerializer.js";
import {OBJLoader2} from "./OBJLoader2.js";

// Imports only related to worker (when standard workers (modules aren't supported) are used)
import {OBJLoader2Parser} from "./obj2/OBJLoader2Parser.js";
import {
  WorkerRunner,
  DefaultWorkerPayloadHandler,
  ObjectManipulator
} from "./obj2/worker/parallel/WorkerRunner.js";


/**
 * Creates a new OBJLoader2Parallel. Use it to load OBJ data from files or to parse OBJ data from arraybuffer.
 * It extends {@link OBJLoader2} with the capability to run the parser in a web worker.
 *
 * @param [LoadingManager] manager The loadingManager for the loader to use. Default is {@link LoadingManager}
 * @constructor
 */
const OBJLoader2Parallel = function (manager) {

  OBJLoader2.call(this, manager);
  this.preferJsmWorker = false;
  this.jsmWorkerUrl = null;

  this.executeParallel = true;
  this.workerExecutionSupport = new WorkerExecutionSupport();

};

OBJLoader2Parallel.OBJLOADER2_PARALLEL_VERSION = '3.2.0';
console.info('Using OBJLoader2Parallel version: ' + OBJLoader2Parallel.OBJLOADER2_PARALLEL_VERSION);
OBJLoader2Parallel.DEFAULT_JSM_WORKER_PATH = './jsm/loaders/obj2/worker/parallel/OBJLoader2JsmWorker.js';

OBJLoader2Parallel.prototype = Object.assign(Object.create(OBJLoader2.prototype), {

  constructor: OBJLoader2Parallel,

  /**
   * Execution of parse in parallel via Worker is default, but normal {OBJLoader2} parsing can be enforced via false here.
   *
   * @param {boolean} executeParallel True or False
   * @return {OBJLoader2Parallel}
   */
  setExecuteParallel: function (executeParallel) {

    this.executeParallel = executeParallel === true;
    return this;

  },

  /**
   * Set whether jsm modules in workers should be used. This requires browser support which is currently only experimental.
   * @param {boolean} preferJsmWorker True or False
   * @param {URL} jsmWorkerUrl Provide complete jsm worker URL otherwise relative path to this module may not be correct
   * @return {OBJLoader2Parallel}
   */
  setJsmWorker: function (preferJsmWorker, jsmWorkerUrl) {

    this.preferJsmWorker = preferJsmWorker === true;

    if (jsmWorkerUrl === undefined || jsmWorkerUrl === null) {

      throw "The url to the jsm worker is not valid. Aborting...";

    }

    this.jsmWorkerUrl = jsmWorkerUrl;

    return this;

  },

  /**
   * Allow to get hold of {@link WorkerExecutionSupport} for configuration purposes.
   * @return {WorkerExecutionSupport}
   */
  getWorkerExecutionSupport: function () {

    return this.workerExecutionSupport;

  },

  /**
   * Provide instructions on what is to be contained in the worker.
   * @return {CodeBuilderInstructions}
   */
  buildWorkerCode: function () {

    const codeBuilderInstructions = new CodeBuilderInstructions(true, true, this.preferJsmWorker);

    if (codeBuilderInstructions.isSupportsJsmWorker()) {

      codeBuilderInstructions.setJsmWorkerUrl(this.jsmWorkerUrl);

    }

    if (codeBuilderInstructions.isSupportsStandardWorker()) {

      const objectManipulator = new ObjectManipulator();
      const defaultWorkerPayloadHandler = new DefaultWorkerPayloadHandler(this.parser);
      const workerRunner = new WorkerRunner({});
      codeBuilderInstructions.addCodeFragment(CodeSerializer.serializeClass(OBJLoader2Parser, this.parser));
      codeBuilderInstructions.addCodeFragment(CodeSerializer.serializeClass(ObjectManipulator, objectManipulator));
      codeBuilderInstructions.addCodeFragment(CodeSerializer.serializeClass(DefaultWorkerPayloadHandler, defaultWorkerPayloadHandler));
      codeBuilderInstructions.addCodeFragment(CodeSerializer.serializeClass(WorkerRunner, workerRunner));

      const startCode = 'new ' + workerRunner.constructor.name + '( new ' + defaultWorkerPayloadHandler.constructor.name + '( new ' + this.parser.constructor.name + '() ) );';
      codeBuilderInstructions.addStartCode(startCode);

    }

    return codeBuilderInstructions;

  },

  /**
   * See {@link OBJLoader2.load}
   */
  load: function (content, onLoad, onFileLoadProgress, onError, onMeshAlter) {

    const scope = this;

    function interceptOnLoad(object3d, message) {

      if (object3d.name === 'OBJLoader2ParallelDummy') {

        if (scope.parser.logging.enabled && scope.parser.logging.debug) {

          console.debug('Received dummy answer from OBJLoader2Parallel#parse');

        }

      } else {

        onLoad(object3d, message);

      }

    }

    OBJLoader2.prototype.load.call(this, content, interceptOnLoad, onFileLoadProgress, onError, onMeshAlter);

  },

  /**
   * See {@link OBJLoader2.parse}
   * The callback onLoad needs to be set to be able to receive the content if used in parallel mode.
   * Fallback is possible via {@link OBJLoader2Parallel#setExecuteParallel}.
   */
  parse: function (content) {

    if (this.executeParallel) {

      if (this.parser.callbacks.onLoad === this.parser._onLoad) {

        throw "No callback other than the default callback was provided! Aborting!";

      }

      // check if worker has been initialize before. If yes, skip init
      if (!this.workerExecutionSupport.isWorkerLoaded(this.preferJsmWorker)) {

        this.workerExecutionSupport.buildWorker(this.buildWorkerCode());

        const scope = this;
        const scopedOnAssetAvailable = function (payload) {

          scope._onAssetAvailable(payload);

        };

        function scopedOnLoad(message) {

          scope.parser.callbacks.onLoad(scope.baseObject3d, message);

        }

        this.workerExecutionSupport.updateCallbacks(scopedOnAssetAvailable, scopedOnLoad);

      }

      // Create default materials beforehand, but do not override previously set materials (e.g. during init)
      this.materialHandler.createDefaultMaterials(false);

      this.workerExecutionSupport.executeParallel(
        {
          params: {
            modelName: this.modelName,
            instanceNo: this.instanceNo,
            useIndices: this.parser.useIndices,
            disregardNormals: this.parser.disregardNormals,
            materialPerSmoothingGroup: this.parser.materialPerSmoothingGroup,
            useOAsMesh: this.parser.useOAsMesh,
            materials: this.materialHandler.getMaterialsJSON()
          },
          data: {
            input: content,
            options: null
          },
          logging: {
            enabled: this.parser.logging.enabled,
            debug: this.parser.logging.debug
          }
        });

      const dummy = new Object3D();
      dummy.name = 'OBJLoader2ParallelDummy';
      return dummy;

    } else {

      return OBJLoader2.prototype.parse.call(this, content);

    }

  },

});

export {OBJLoader2Parallel};
